﻿using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Serilog;
using Serilog.Events;
using Serilog.Extensions.Hosting;
using Serilog.Parsing;
using System;
using System.Diagnostics;
using System.Net;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Web;
using TK.Common.AspNetCore;
using ILogger = Serilog.ILogger;

namespace TK.Common.Logger
{
    class RequestLoggingMiddleware
    {
        readonly RequestDelegate _next;
        readonly DiagnosticContext _diagnosticContext;
        readonly MessageTemplate _messageTemplate;
        readonly Action<IDiagnosticContext, HttpContext>? _enrichDiagnosticContext;
        readonly Func<HttpContext, double, Exception?, LogEventLevel> _getLevel;
        readonly Func<HttpContext, string, double, int, IEnumerable<LogEventProperty>> _getMessageTemplateProperties;
        readonly ILogger? _logger;
        readonly bool _includeQueryInRequestPath;
        readonly bool _includeRequestBody;
        readonly Func<bool, int, string, object> _exceptionResult;
        readonly Func<HttpContext, bool> _writeLog;
        static readonly LogEventProperty[] NoProperties = new LogEventProperty[0];
        static string traceKey = "TraceId";

        public RequestLoggingMiddleware(
            RequestDelegate next,
            DiagnosticContext diagnosticContext,
            RequestLoggingOptions options)
        {
            if (options == null) throw new ArgumentNullException(nameof(options));
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));
            _getLevel = options.GetLevel;
            traceKey = RequestLoggingOptions.TraceKey;
            _enrichDiagnosticContext = options.EnrichDiagnosticContext;
            _messageTemplate = new MessageTemplateParser().Parse(options.MessageTemplate);
            _logger = options.Logger?.ForContext<RequestLoggingMiddleware>();
            _includeQueryInRequestPath = options.IncludeQueryInRequestPath;
            _getMessageTemplateProperties = options.GetMessageTemplateProperties;
            _includeRequestBody = options.IncludeRequestBody;
            _writeLog = options.WriteLog ?? (context => true);
            _exceptionResult = options.ExceptionResult ?? ((success, statusCode, msg) => UnaryResult.Create(success, statusCode, msg));
        }

        /// <summary>
        /// Invoke
        /// </summary>
        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
            SetTraceId(httpContext);
            var start = Stopwatch.GetTimestamp();
            var collector = _diagnosticContext.BeginCollection();
            try
            {
                await SetLogContext(httpContext);
                await _next(httpContext);
                var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp());
                var statusCode = httpContext.Response.StatusCode;
                LogCompletion(httpContext, collector, statusCode, elapsedMs, null);
                if (httpContext.Response.StatusCode == 401 || httpContext.Response.StatusCode == 403)
                {
                    var exception = _exceptionResult(false, httpContext.Response.StatusCode, "验证授权未通过");
                    await WriteErrorResponseAsync(httpContext, exception);
                }
            }
            catch (Exception ex)
            {
                object exception = null;

                if (ex is BusinessException)
                {
                    httpContext.Response.StatusCode = (int)HttpStatusCode.ExpectationFailed;
                    exception = _exceptionResult(false, httpContext.Response.StatusCode, ex.Message);
                }
                else
                {
                    httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    LogCompletion(httpContext, collector, httpContext.Response.StatusCode, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), ex);
                    exception = _exceptionResult(false, httpContext.Response.StatusCode, "服务内部异常");
                }

                await WriteErrorResponseAsync(httpContext, exception);
            }
            finally
            {
                collector.Dispose();
            }
        }

        #region private methods
        bool LogCompletion(HttpContext httpContext, DiagnosticContextCollector collector, int statusCode, double elapsedMs, Exception? ex)
        {
            if (!_writeLog(httpContext))
            {
                return false;
            }

            var logger = _logger ?? Log.ForContext<RequestLoggingMiddleware>();
            var level = _getLevel(httpContext, elapsedMs, ex);
            if (!logger.IsEnabled(level)) return false;

            // Enrich diagnostic context
            _enrichDiagnosticContext?.Invoke(_diagnosticContext, httpContext);
            if (!collector.TryComplete(out var collectedProperties, out var collectedException))
                collectedProperties = NoProperties;

            // Last-in (correctly) wins...
            var properties = collectedProperties.Concat(_getMessageTemplateProperties(httpContext, GetPath(httpContext, _includeQueryInRequestPath), elapsedMs, statusCode));
            var evt = new LogEvent(DateTimeOffset.Now, level, ex ?? collectedException, _messageTemplate, properties);
            logger.Write(evt);

            return false;
        }

        /// <summary>
        /// 增加日志上下文
        /// </summary>
        private async Task SetLogContext(HttpContext httpContext)
        {
            _diagnosticContext.Set("Header", httpContext.Request.Headers);
            var request = httpContext.Request;
            var url = $"{request.Scheme}://{request.Host}{request.Path}{HttpUtility.UrlDecode(request.QueryString.Value)}|{request.Protocol}";
            _diagnosticContext.Set(RequestLoggingOptions.TraceKey, httpContext.TraceIdentifier);
            _diagnosticContext.Set("Url", url);
            _diagnosticContext.Set("RequestMethod", request.Method);
            _diagnosticContext.Set("StatusCode", httpContext.Response.StatusCode);
            if (_includeRequestBody)
            {
                var dic = await ToRequestContent(httpContext);
                _diagnosticContext.Set("RequestData", dic);
            }
        }

        static double GetElapsedMilliseconds(long start, long stop)
        {
            return (stop - start) * 1000 / (double)Stopwatch.Frequency;
        }

        static string GetPath(HttpContext httpContext, bool includeQueryInRequestPath)
        {
            /*
                In some cases, like when running integration tests with WebApplicationFactory<T>
                the Path returns an empty string instead of null, in that case we can't use
                ?? as fallback.
            */
            var requestPath = includeQueryInRequestPath
                ? httpContext.Features.Get<IHttpRequestFeature>()?.RawTarget
                : httpContext.Features.Get<IHttpRequestFeature>()?.Path;
            if (string.IsNullOrEmpty(requestPath))
            {
                requestPath = httpContext.Request.Path.ToString();
            }

            return requestPath!;
        }

        /// <summary>
        /// 获取设置分布式TraceIdentifier
        /// </summary>
        public static void  SetTraceId(HttpContext httpContext)
        {
            if (httpContext.Request.Headers.ContainsKey(traceKey))
            {
                httpContext.TraceIdentifier = httpContext.Request.Headers[traceKey];
            }

            if (string.IsNullOrWhiteSpace(httpContext.TraceIdentifier))
            {
                httpContext.TraceIdentifier = Guid.NewGuid().ToString();
            }

            httpContext.Response.OnStarting(() =>
            {
                if (!httpContext.Response.Headers.ContainsKey(traceKey))
                {
                    httpContext.Response.Headers.Add(traceKey, httpContext.TraceIdentifier);
                }

                return Task.CompletedTask;
            });
        }

        /// <summary>
        /// 获取请求体内容
        /// </summary>
        private async Task<Dictionary<string, string>> ToRequestContent(HttpContext context)
        {
            var result = new Dictionary<string, string>();
            var request = context.Request;
            if (request != null)
            {
                try
                {
                    if (request.HasJsonContentType())
                    {
                        request.EnableBuffering();
                        request.Body.Position = 0;
                        var reader = new StreamReader(request.Body, leaveOpen: true);
                        var data = await reader.ReadToEndAsync();
                        request.Body.Position = 0;
                        result.TryAdd("data", data);
                    }

                    if (request.HasFormContentType)
                    {
                        foreach (var item in request.Form)
                        {
                            result.TryAdd($"form-{item.Key}", item.Value);
                        }

                        if (request.Form.Files != null)
                        {
                            for (var i = 0; i < request.Form.Files.Count; i++)
                            {
                                var formFile = request.Form.Files[i];
                                result.TryAdd($"{formFile.FileName}-{i + 1}", $"{formFile.ContentDisposition}_{formFile.Length}");
                            }
                        }
                    }
                }
                catch { }
            }

            return result;
        }

        /// <summary>
        /// 输出流
        /// </summary>
        private async Task WriteErrorResponseAsync(HttpContext context, object result)
        {
            var format = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                WriteIndented = false,
                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
            };
            context.Response.ContentType = "text/json;charset=utf-8;";
            await context.Response.WriteAsync(JsonSerializer.Serialize(result, format));
        }
        #endregion
    }

}
